All files / web/src/app/api/teacher-flowcharts/[id]/publish route.ts

0% Statements 0/128
0% Branches 0/1
0% Functions 0/1
0% Lines 0/128

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129                                                                                                                                                                                                                                                                 
import { and, eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { db, schema } from '@/db'
import { getUserId } from '@/lib/viewer'
import { generateFlowchartEmbeddings, EMBEDDING_VERSION } from '@/lib/flowcharts/embedding'
import { invalidateEmbeddingCache } from '@/lib/flowcharts/embedding-search'
import { withAuth } from '@/lib/auth/withAuth'

/**
 * POST /api/teacher-flowcharts/[id]/publish
 * Publish a draft flowchart
 *
 * Validates the flowchart before publishing:
 * - Definition JSON must be valid
 * - Required fields must be present
 *
 * Returns: { flowchart: TeacherFlowchart }
 */
export const POST = withAuth(async (_request, { params }) => {
  try {
    const { id } = (await params) as { id: string }
    const userId = await getUserId()

    // Find the flowchart
    const existing = await db.query.teacherFlowcharts.findFirst({
      where: and(eq(schema.teacherFlowcharts.id, id), eq(schema.teacherFlowcharts.userId, userId)),
    })

    if (!existing) {
      return NextResponse.json({ error: 'Flowchart not found' }, { status: 404 })
    }

    if (existing.status === 'published') {
      return NextResponse.json({ error: 'Flowchart is already published' }, { status: 400 })
    }

    if (existing.status === 'archived') {
      return NextResponse.json({ error: 'Cannot publish an archived flowchart' }, { status: 400 })
    }

    // Validate the definition before publishing
    let definition
    try {
      definition = JSON.parse(existing.definitionJson)
    } catch {
      return NextResponse.json({ error: 'Invalid flowchart definition JSON' }, { status: 400 })
    }

    // Basic validation
    const validationErrors: string[] = []

    if (!definition.id) validationErrors.push('Missing flowchart ID in definition')
    if (!definition.title) validationErrors.push('Missing title in definition')
    if (!definition.entryNode) validationErrors.push('Missing entry node')
    if (!definition.nodes || Object.keys(definition.nodes).length === 0) {
      validationErrors.push('Flowchart must have at least one node')
    }
    if (!definition.problemInput) {
      validationErrors.push('Missing problem input schema')
    }

    if (validationErrors.length > 0) {
      return NextResponse.json(
        { error: 'Validation failed', details: validationErrors },
        { status: 400 }
      )
    }

    // TODO: Add more comprehensive validation:
    // - Verify mermaid parses correctly
    // - Verify all node references are valid
    // - Verify expressions are valid
    // - Test with example generation

    // Find the workshop session to get the original topic description (if any)
    // This helps with semantic matching - the user's original phrasing captures intent
    const workshopSession = await db.query.workshopSessions.findFirst({
      where: eq(schema.workshopSessions.flowchartId, id),
      columns: {
        topicDescription: true,
      },
    })

    // Generate embeddings for semantic search
    // - embedding: full content (title + description + topic + difficulty)
    // - promptEmbedding: just the original topic description (better for short queries)
    let embedding: Buffer | null = null
    let promptEmbedding: Buffer | null = null
    try {
      const result = await generateFlowchartEmbeddings({
        title: existing.title,
        description: existing.description,
        topicDescription: workshopSession?.topicDescription,
        difficulty: existing.difficulty,
      })
      embedding = result.embedding
      promptEmbedding = result.promptEmbedding
    } catch (embeddingError) {
      // Log but don't fail publish - embedding is nice to have but not required
      console.error('Failed to generate embeddings for flowchart:', embeddingError)
    }

    const now = new Date()

    const [flowchart] = await db
      .update(schema.teacherFlowcharts)
      .set({
        status: 'published',
        publishedAt: now,
        updatedAt: now,
        embedding,
        promptEmbedding,
        embeddingVersion: embedding ? EMBEDDING_VERSION : null,
      })
      .where(eq(schema.teacherFlowcharts.id, id))
      .returning()

    // Invalidate embedding cache so new flowchart appears in searches
    if (embedding) {
      invalidateEmbeddingCache()
    }

    return NextResponse.json({ flowchart })
  } catch (error) {
    console.error('Failed to publish teacher flowchart:', error)
    return NextResponse.json({ error: 'Failed to publish flowchart' }, { status: 500 })
  }
})